Java 破坏双亲委派机制

类加载分为7个步骤,分别是:加载,验证,准备,解析,初始化,使用,卸载。加载阶段是jvm将class字节码文件将类装载到内存当中,执行这一操作的是类加载器。

系统由三种类加载器

  1. BootStrap ClassLoader, 启动类加载器,负责加载Java的核心类库,如/lib/rt.jar,lib/resources.jar等,它由C++编写

  2. Extension ClassLoader, 扩展类加载器,负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar。

  3. App ClassLoader, 系统类加载器,负责加载应用程序classpath目录下的所有jar和class文件。

  4. 自定义类加载器,一般继承3,实现自定义的类加载器

一般的,Java采用双亲委派机制进行类加载,即首先委派父类加载器进行加载,如果父类能够加载,则自己不用加载,否则尝试自己加载。这样的好处就是,对于同一个类,如核心类,都是由同一个类加载器进行加载。在判断两个类是否相同时,除了比较它们的全限定名,还是比较他们的类加载器,不同的类加载器加载同一份字节码,在系统中也会被视为不同的类。

而,本文,要讨论的就是破坏双亲委派机制。试想如果在核心类中,想要加载一个用户自己编写的类,那么,由于核心类是由启动类加载器加载的,根据双亲委派机制,它无法委派自己的孩子-系统类加载器进行加载,那该如何解决呢?

事实上,这种情况经常发生。

JNDI服务:JNDI的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现部部署在应用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代码。

因此,为了破坏这种双亲委派机制,在启动类里也能够使用系统类加载器对类进行加载,Java设计了线程上下文加载器。在核心类进行类加载时,可以读取当前线程的线程上下文加载器,使用该加载器加载实现了SPI接口的类。

Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI,JDBC,JCE,JAXB和JBI等

JDBC

JDBC是一个典型的破坏了双亲委派机制的案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class JdbcUtils {
static {
try {
// (1)
Class.forName("com.mysql.jdbc.Driver").newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
public static Connection getConnection(String url) throws SQLException {
// (2)
return DriverManager.getConnection(url);
}
}

DriverManager是Java的核心类,

1
2
3
4
public static Conection getConnection(String url) throws SQLException{
java.util.Properties info = new java.util.Properties();
return (getConnection(url, info, Reflection.getCallerClass()));
}

getConnection函数里,得到了调用者对象的类,这里是JdbcUtils,因此可以在rt类的代码里使用JdbcUtils的ClassLoader进行类加载。但是源码其实并没这么做,注意到我们自己写的类中有Class.forName("com.mysql.jdbc.Driver").newInstance();这句话了吗?这里已经加载了一个Driver,MYSQL的Driver,而在DriverManager中,会遍历所有Driver,使用Class clazz = Class.forName(Driver.getClass().getName,true,classLoader),使用传进来的classLoader对全有驱动的 全限定名进行重新加载,在比较clazz和Driver.getClass()是否相等。

tomcat

WebappClassLoader内部重写了loadClass和findClass方法,实现了绕过“双亲委派”直接加载web应用内部的资源,当然可以通过在Context.xml文件中加上开启正统的“双亲委派”加载机制